package org.codefilarete.stalactite.engine.listener;

import java.util.Set;
import java.util.function.Supplier;

import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.function.ThrowingExecutable;
import org.codefilarete.tool.function.ThrowingRunnable;

/**
 * Simple class that centralizes persistence event listener registration as well as execution of some code wrapped between event triggering.
 * 
 * @author Guillaume Mary
 * @see #doWithInsertListener(Iterable, ThrowingExecutable)
 * @see #doWithUpdateListener(Iterable, boolean, Supplier)
 * @see #doWithUpdateByIdListener(Iterable, ThrowingExecutable)
 * @see #doWithDeleteListener(Iterable, ThrowingExecutable)
 * @see #doWithDeleteByIdListener(Iterable, ThrowingExecutable)
 * @see #doWithSelectListener(Iterable, ThrowingExecutable)
 */
public class PersisterListenerCollection<C, I> implements PersisterListener<C, I> {
	
	private final PersistListenerCollection<C> persistListener = new PersistListenerCollection<>();
	private final InsertListenerCollection<C> insertListener = new InsertListenerCollection<>();
	private final UpdateByIdListenerCollection<C> updateByIdListener = new UpdateByIdListenerCollection<>();
	private final UpdateListenerCollection<C> updateListener = new UpdateListenerCollection<>();
	private final DeleteListenerCollection<C> deleteListener = new DeleteListenerCollection<>();
	private final DeleteByIdListenerCollection<C> deleteByIdListener = new DeleteByIdListenerCollection<>();
	private final SelectListenerCollection<C, I> selectListener = new SelectListenerCollection<>();
	
	public InsertListenerCollection<C> getInsertListener() {
		return insertListener;
	}
	
	@Override
	public void addPersistListener(PersistListener<? extends C> persistListener) {
		this.persistListener.add(persistListener);
	}
	
	public <R> R doWithPersistListener(Iterable<? extends C> entities, ThrowingExecutable<R, RuntimeException> delegate) {
		R result;
		try {
			persistListener.beforePersist(entities);
			result = delegate.execute();
			persistListener.afterPersist(entities);
		} catch (RuntimeException e) {
			persistListener.onPersistError(entities, e);
			throw e;
		}
		return result;
	}
	
	public void doWithPersistListener(Iterable<? extends C> entities, ThrowingRunnable<RuntimeException> delegate) {
		try {
			persistListener.beforePersist(entities);
			delegate.run();
			persistListener.afterPersist(entities);
		} catch (RuntimeException e) {
			persistListener.onPersistError(entities, e);
			throw e;
		}
	}
	
	@Override
	public void addInsertListener(InsertListener<? extends C> insertListener) {
		this.insertListener.add(insertListener);
	}
	
	public <R> R doWithInsertListener(Iterable<? extends C> entities, ThrowingExecutable<R, RuntimeException> delegate) {
		R result;
		try {
			insertListener.beforeInsert(entities);
			result = delegate.execute();
			insertListener.afterInsert(entities);
		} catch (RuntimeException e) {
			insertListener.onInsertError(entities, e);
			throw e;
		}
		return result;
	}
	
	public void doWithInsertListener(Iterable<? extends C> entities, ThrowingRunnable<RuntimeException> delegate) {
		try {
			insertListener.beforeInsert(entities);
			delegate.run();
			insertListener.afterInsert(entities);
		} catch (RuntimeException e) {
			insertListener.onInsertError(entities, e);
			throw e;
		}
	}
	
	public UpdateByIdListenerCollection<C> getUpdateByIdListener() {
		return updateByIdListener;
	}
	
	@Override
	public void addUpdateByIdListener(UpdateByIdListener<? extends C> updateByIdListener) {
		this.updateByIdListener.add((UpdateByIdListener<C>) updateByIdListener);
	}
	
	public <R> R doWithUpdateByIdListener(Iterable<? extends C> entities, ThrowingExecutable<R, RuntimeException> delegate) {
		R result;
		try {
			updateByIdListener.beforeUpdateById(entities);
			result = delegate.execute();
			updateByIdListener.afterUpdateById(entities);
		} catch (RuntimeException e) {
			updateByIdListener.onUpdateError(entities, e);
			throw e;
		}
		return result;
	}
	
	public void doWithUpdateByIdListener(Iterable<? extends C> entities, ThrowingRunnable<RuntimeException> delegate) {
		try {
			updateByIdListener.beforeUpdateById(entities);
			delegate.run();
			updateByIdListener.afterUpdateById(entities);
		} catch (RuntimeException e) {
			updateByIdListener.onUpdateError(entities, e);
			throw e;
		}
	}
	
	public UpdateListenerCollection<C> getUpdateListener() {
		return updateListener;
	}
	
	@Override
	public void addUpdateListener(UpdateListener<? extends C> updateListener) {
		this.updateListener.add((UpdateListener<C>) updateListener);
	}
	
	public <R, T extends Table<T>> R doWithUpdateListener(Iterable<? extends Duo<C, C>> differencesIterable, boolean allColumnsStatement,
														  Supplier<R> delegate) {
		R result;
		try {
			updateListener.beforeUpdate(differencesIterable, allColumnsStatement);
			result = delegate.get();
			updateListener.afterUpdate(differencesIterable, allColumnsStatement);
		} catch (RuntimeException e) {
			updateListener.onUpdateError(Iterables.collectToList(differencesIterable, Duo::getLeft), e);
			throw e;
		}
		return result;
	}
	
	public <T extends Table<T>> void doWithUpdateListener(Iterable<? extends Duo<C, C>> differencesIterable, boolean allColumnsStatement,
														  Runnable delegate) {
		try {
			updateListener.beforeUpdate(differencesIterable, allColumnsStatement);
			delegate.run();
			updateListener.afterUpdate(differencesIterable, allColumnsStatement);
		} catch (RuntimeException e) {
			updateListener.onUpdateError(Iterables.collectToList(differencesIterable, Duo::getLeft), e);
			throw e;
		}
	}
	
	public DeleteListenerCollection<C> getDeleteListener() {
		return deleteListener;
	}
	
	public DeleteByIdListenerCollection<C> getDeleteByIdListener() {
		return deleteByIdListener;
	}
	
	@Override
	public void addDeleteListener(DeleteListener<? extends C> deleteListener) {
		this.deleteListener.add(deleteListener);
	}
	
	public <R> R doWithDeleteListener(Iterable<? extends C> entities, ThrowingExecutable<R, RuntimeException> delegate) {
		R result;
		try {
			deleteListener.beforeDelete(entities);
			result = delegate.execute();
			deleteListener.afterDelete(entities);
		} catch (RuntimeException e) {
			deleteListener.onDeleteError(entities, e);
			throw e;
		}
		return result;
	}
	
	public void doWithDeleteListener(Iterable<? extends C> entities, ThrowingRunnable<RuntimeException> delegate) {
		try {
			deleteListener.beforeDelete(entities);
			delegate.run();
			deleteListener.afterDelete(entities);
		} catch (RuntimeException e) {
			deleteListener.onDeleteError(entities, e);
			throw e;
		}
	}
	
	@Override
	public void addDeleteByIdListener(DeleteByIdListener<? extends C> deleteByIdListener) {
		this.deleteByIdListener.add(deleteByIdListener);
	}
	
	public <R> R doWithDeleteByIdListener(Iterable<? extends C> entities, ThrowingExecutable<R, RuntimeException> delegate) {
		R result;
		try {
			deleteByIdListener.beforeDeleteById(entities);
			result = delegate.execute();
			deleteByIdListener.afterDeleteById(entities);
		} catch (RuntimeException e) {
			deleteByIdListener.onDeleteError(entities, e);
			throw e;
		}
		return result;
	}
	
	public void doWithDeleteByIdListener(Iterable<? extends C> entities, ThrowingRunnable<RuntimeException> delegate) {
		try {
			deleteByIdListener.beforeDeleteById(entities);
			delegate.run();
			deleteByIdListener.afterDeleteById(entities);
		} catch (RuntimeException e) {
			deleteByIdListener.onDeleteError(entities, e);
			throw e;
		}
	}
	
	public SelectListenerCollection<C, I> getSelectListener() {
		return selectListener;
	}
	
	@Override
	public void addSelectListener(SelectListener<? extends C, I> selectListener) {
		this.selectListener.add(selectListener);
	}
	
	public Set<C> doWithSelectListener(Iterable<I> ids, ThrowingExecutable<Set<C>, RuntimeException> delegate) {
		Set<C> result;
		try {
			selectListener.beforeSelect(ids);
			result = delegate.execute();
			selectListener.afterSelect(result);
		} catch (RuntimeException e) {
			selectListener.onSelectError(ids, e);
			throw e;
		}
		return result;
	}
	
	/**
	 * Move internal listeners to given instance.
	 * Useful to aggregate listeners into a single instance.
	 * Please note that as this method is named "move" it means that listeners of current instance will be cleared.
	 * 
	 * @param persisterListener the target listener on which the one of current instance must be moved to.
	 */
	public void moveTo(PersisterListenerCollection<C, I> persisterListener) {
		this.persistListener.moveTo(persisterListener.persistListener);
		this.insertListener.moveTo(persisterListener.insertListener);
		this.updateByIdListener.moveTo(persisterListener.updateByIdListener);
		this.updateListener.moveTo(persisterListener.updateListener);
		this.deleteListener.moveTo(persisterListener.deleteListener);
		this.deleteByIdListener.moveTo(persisterListener.deleteByIdListener);
		this.selectListener.moveTo(persisterListener.selectListener);
	}
}
